package cn.ratel.db;

import cn.ratel.component.Constants;
import cn.ratel.kit.CollectionKit;
import com.google.common.base.Preconditions;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;
import java.sql.*;
import java.util.*;

/**
 * author iver
 * ======================================================================================================
 * <p>
 * ===关于命名规则：
 * 1.table采用小写下划线命名，column采用小驼峰式命名
 * 2.所有的cache方法都已经自动添加前缀（详见 - cacheKey方法）【表名.】
 * 3.任何一个表都有一个名为 id 的主键
 * <p>
 * ===关于序列化规则（针对缓存 - JsonKit和SerializerKit）：
 * 1.此时把model当成承载db的对象，所以序列化后，只存放了db相关字段，非db字段则被忽略
 * <p>
 * ===关于模版取值规则：
 * 1.在model的子类添加getter方法
 * <p>
 * ===关于序列化规则（针对json接口）：
 * 1.使用attrs属性，把attrs这个map对象转换为json
 * <p>
 * ======================================================================================================
 */
@SuppressWarnings({"rawtypes", "unchecked"})
public abstract class Model<M extends Model> implements Serializable {
	protected static Logger log = LoggerFactory.getLogger(Model.class);

	private static final Object[] NULL_PARAMS = new Object[0];

	private final Map<String, Object> attrs = new HashMap<>();
	private transient Set<String> modifiedAttrs;

	public void clearModifiedAttrs() {
		this.modifiedAttrs = null;
	}

	// --------------------------- attrs
	public final Map<String, Object> attrs() {
		return this.attrs;
	}

	public final Map<String, Object> attrsColumnOnly() {
		int initialCapacity = this.attrs.size() * 4 / 3 + 1;
		Map<String, Object> map = new HashMap<>(initialCapacity);
		this.attrs.entrySet().stream().filter(entry -> table().contains(entry.getKey())).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
		return map;
	}

	public final M set(String columnName, Object value) {
		if (table().contains(columnName)) {
			if (modifiedAttrs == null) {
				modifiedAttrs = new HashSet<>();
			}
			attrs.put(columnName, value);
			modifiedAttrs.add(columnName);
		} else {
			log.warn("not exist this column : " + columnName);
		}
		return (M) this;
	}

	public final M set(Map<String, Object> attrs) {
		if (attrs == null) {
			return (M) this;
		}
		for (Map.Entry<String, Object> entry : attrs.entrySet()) {
			set(entry.getKey(), entry.getValue());
		}
		return (M) this;
	}

	public final M put(String attr, Object value) {
		this.attrs.put(attr, value);
		return (M) this;
	}

	public final M put(Map<String, Object> attrs) {
		this.attrs.putAll(attrs);
		return (M) this;
	}

	public final <T> T get(String attr) {
		return (T) attrs.get(attr);
	}

	public final <T> T get(String attr, T defaultValue) {
		return attrs.get(attr) == null ? defaultValue : (T) attrs.get(attr);
	}

	public final M clear() {
		attrs.clear();
		return (M) this;
	}

	public final boolean hasColumn(String columnName) {
		return table().columns.containsKey(columnName);
	}

	// --------------------------- count
	public final long count(String sql, Object... params) {
		M result = findOne(sql, params);
		Map<String, Object> attrs = result.attrs();
		Preconditions.checkArgument(attrs.size() == 1, "more than one column returned !");
		long count = 0;
		for (Map.Entry<String, Object> entry : attrs.entrySet()) {
			count = (long) entry.getValue();
		}
		return count;
	}

	// --------------------------- find
	public final M find(int id) {
		String sql = Dialect.findById(table());
		List<M> result = find(sql, id);
		return CollectionKit.isEmpty(result) ? null : result.get(0);
	}

	public final List<M> findAll() {
		return find(newSql());
	}

	public final List<M> find(Model model) {
		Preconditions.checkArgument(!model.attrs().isEmpty(), "attrs of model can not be empty");
		Dialect.SqlAndParams sqlAndParams = Dialect.find(table(), model.attrs());
		return find(sqlAndParams.sql, sqlAndParams.params);
	}

	public final M findOne(Model model) {
		return this.first(this.find(model));
	}

	public final List<M> find(Sqls sqls) {
		return find(sqls.sql(), sqls.params());
	}

	public final M findOne(Sqls sqls) {
		return this.first(this.find(sqls));
	}

	public final List<M> find(String sql, Object... params) {
		return executeQuery(rs -> ModelBuilder.build(rs, this.getClass()), sql, params);
	}

	public final M findOne(String sql, Object... params) {
		return this.first(this.find(sql, params));
	}

	public final Page<M> paginate(Sqls sqls, int pageNumber, int pageSize) {
		return paginate(sqls.sql(), pageNumber, pageSize, sqls.params());
	}

	public final Page<M> paginate(String sql, int pageNumber, int pageSize, Object... params) {
		sql = Dialect.paginateList(sql, pageNumber, pageSize);

		Connection conn = DbKit.getConnection();
		List<M> list;
		List<M> totalRow;
		try {
			list = executeQuery(conn, rs -> ModelBuilder.build(rs, this.getClass()), sql, params);
			totalRow = executeQuery(conn, rs -> ModelBuilder.build(rs, this.getClass()), Dialect.paginateCount());
		} finally {
			DbKit.close(conn);
		}

		int count = CollectionKit.isEmpty(totalRow) ? -1 : Integer.parseInt(totalRow.get(0).get("totalRow").toString());
		return count == 0 ? null : new Page<>(list, pageNumber, pageSize, count);
	}

	// --------------------------- save
	public final boolean save() {
		attrs.remove("id");
		Dialect.SqlAndParams sqlAndParams = Dialect.save(table(), attrs);
		return executeUpdate(sqlAndParams.sql, true, sqlAndParams.params) >= 1;
	}

	// --------------------------- update
	public final boolean update() {
		Preconditions.checkArgument(CollectionKit.isNotEmpty(modifiedAttrs), "no column has been set");
		Dialect.SqlAndParams sqlAndParams = Dialect.update(table(), attrs, modifiedAttrs);
		int result = executeUpdate(sqlAndParams.sql, false, sqlAndParams.params);
		modifiedAttrs.clear();
		return result >= 1;
	}

	public final boolean saveOrUpdate() {
		if (find(id()) == null) {
			return this.save();
		} else {
			return this.update();
		}
	}

	// --------------------------- delete
	public final boolean delete() {
		return delete(id());
	}

	public final boolean delete(int id) {
		String sql = Dialect.deleteById(table());
		return executeUpdate(sql, false, id) >= 1;
	}

	// --------------------------- table
	private Table table() {
		return TableBuilder.getTable(this.getClass());
	}

	public final String tableName() {
		return TableBuilder.getTable(this.getClass()).name;
	}

	// --------------------------- sql
	public final Sqls newSql() {
		return new Sqls(tableName());
	}

	// --------------------------- query
	private List<M> executeQuery(ResultSetHandler resultSetHandler, String sql, Object... params) {
		List<M> result = null;
		Connection conn = null;
		params = params == null ? NULL_PARAMS : params;
		try {
			conn = DbKit.getConnection();
			result = executeQuery(conn, resultSetHandler, sql, params);
		} finally {
			DbKit.close(conn);
		}
		return result;
	}

	private List<M> executeQuery(Connection conn, ResultSetHandler resultSetHandler, String sql, Object... params) {
		List<M> result;
		try {
			PreparedStatement pst = conn.prepareStatement(sql);
			reportSql(sql, params);
			fillStatement(pst, params);
			ResultSet rs = pst.executeQuery();
			result = (List<M>) resultSetHandler.handle(rs);
			DbKit.close(rs, pst);
		} catch (SQLException e) {
			throw new RuntimeException("executeQuery error", e);
		}
		return result;
	}

	private int executeUpdate(String sql, boolean isInsert, Object... params) {
		int result;
		Connection conn = null;
		try {
			conn = DbKit.getConnection();
			PreparedStatement pst;
			if (isInsert) {
				pst = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
			} else {
				pst = conn.prepareStatement(sql);
			}
			reportSql(sql, params);
			fillStatement(pst, params);
			result = pst.executeUpdate();
			if (isInsert) {
				setIdAfterSave(pst);
			}
			DbKit.close(pst);
		} catch (SQLException e) {
			throw new RuntimeException("executeUpdate error : ", e);
		} finally {
			DbKit.close(conn);
		}
		return result;
	}

	private void setIdAfterSave(PreparedStatement pst) throws SQLException {
		ResultSet rs = pst.getGeneratedKeys();
		if (rs.next()) {
			Object id = rs.getObject(1);
			this.set("id", Integer.parseInt(id + ""));
		}
	}

	private void fillStatement(PreparedStatement pst, Object... params) throws SQLException {
		if (params == null) {
			return;
		}
		for (int i = 0; i < params.length; i++) {
			pst.setObject(i + 1, params[i]);
		}
	}

	private void reportSql(String sql, Object... params) {
		if (Constants.me().isDevMode()) {
			if (params == null || params.length == 0) {
				log.info("[sql] => SQL：{}", sql);
			} else {
				log.info("[sql] => SQL：{} ## PARAMS：{}", sql, ArrayUtils.toString(params));
			}
		}
	}

	public final int id() {
		Object id = attrs.get("id");
		Preconditions.checkNotNull(id, "id不能为空");
		return (int) id;
	}

	private M first(List<M> list) {
		return CollectionKit.isEmpty(list) ? null : list.get(0);
	}

	@Override
	public String toString() {
		return tableName() + ":" + attrs;
	}

	private interface ResultSetHandler<M> {
		List<M> handle(ResultSet rs);
	}

}
